package com.gitee.wsl.compose.chart.pie

import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.gitee.wsl.compose.chart.Chart
import com.gitee.wsl.compose.chart.DegreesFullCircle
import com.gitee.wsl.compose.chart.ExperimentalKoalaPlotApi
import com.gitee.wsl.compose.chart.KoalaPlotTheme
import com.gitee.wsl.compose.chart.SelectChangedListener
import com.gitee.wsl.compose.chart.SelectCheck
import com.gitee.wsl.compose.chart.animation.StartAnimationUseCase
import com.gitee.wsl.compose.chart.circumscribedSquareSize
import com.gitee.wsl.compose.chart.frond.LabelConnector
import com.gitee.wsl.compose.chart.frond.LabelConnectorScope
import com.gitee.wsl.compose.chart.frond.LabelConnectorSlate
import com.gitee.wsl.compose.chart.frond.LabelSlate
import com.gitee.wsl.compose.chart.frond.Straight
import com.gitee.wsl.compose.chart.frond.StraightLineConnector
import com.gitee.wsl.compose.chart.generateHueColorPalette
import com.gitee.wsl.compose.chart.pie.createDefaultSlice
import com.gitee.wsl.compose.chart.pie.layout.PieMeasurePolicy
import com.gitee.wsl.compose.chart.pie.label.CircularLabelPositionProvider
import com.gitee.wsl.compose.chart.pie.label.LabelPositionProvider
import com.gitee.wsl.compose.chart.pie.layout.PiePlaceables
import com.gitee.wsl.compose.chart.pie.model.PieSliceData
import com.gitee.wsl.compose.chart.pie.model.makePieSliceData
import com.gitee.wsl.compose.ui.container.hoverable.HoverableElementArea
import com.gitee.wsl.compose.ui.container.hoverable.HoverableElementAreaScope
import com.gitee.wsl.unit.angle.AngleUnit
import com.gitee.wsl.unit.angle.deg
import com.gitee.wsl.unit.angle.toDegrees

import kotlin.math.min

private const val DefaultLabelDiameterScale = 1.1f

// Initial outer radius as a fraction of size before hover expansion
private const val InitOuterRadius = 0.95f

private const val LabelFadeInDuration = 1000

private const val AngleCCWTop = -90f


/**
 * Creates a Pie Chart or, optionally, a Donut Chart if holeSize is nonZero, with optional
 * hole content to place at the center of the donut hole. Pie slices are drawn starting at
 * -90 degrees (top center), progressing clockwise around the pie. Each slice occupies a fraction
 * of the overall pie according to its data value relative to the sum of all values.
 *
 * @param values The data values for each pie slice
 * @param labelPositionProvider A provider of label offsets that can be used to implement different label
 * placement strategies. See the [PieChart] override for a version that uses labels placed around the circumference
 * of the pie.
 * @param modifier Compose Modifiers to be applied to the overall PieChart
 * @param slice Composable for a pie slice.
 * @param label Composable for a pie slice label placed around the perimeter of the pie
 * @param labelConnector Composable for label connectors connecting the pie slice to the label
 * @param holeSize A relative size for an inner hole of the pie, creating a donut chart, with a
 * value between 0 and 1.
 * @param holeContent Optional content that may be placed in the space of the donut hole. To safely draw the content
 * within the hole without intersecting with the chart, apply the passed `contentPadding` to your content composable.
 * @param minPieDiameter Minimum diameter allowed for the pie.
 * @param maxPieDiameter Maximum diameter allowed for the pie. May be Infinity but not Unspecified.
 * @param forceCenteredPie If true, will force the pie to be centered within its parent, by adjusting (decreasing) the
 * pie size to accommodate label sizes and positions. If false, will maximize the pie diameter.
 * @param pieAnimationSpec Specifies the animation to use when the pie chart is first drawn.
 * @param labelAnimationSpec Specifies the animation to use when the labels are drawn. Drawing of the labels begins
 * after the pie animation is complete.
 * @param pieStartAngle Sets an angle for the pie data to start at. Defaults to the top of the pie.
 * @param pieExtendAngle Sets a max angle for the pie to extend to, with a value between 1 and 360.
 * Defaults to [DegreesFullCircle].
 */
@ExperimentalKoalaPlotApi
@Composable
fun Chart.Pie(
    values: List<Float>,
    labelPositionProvider: LabelPositionProvider,
    modifier: Modifier = Modifier,
    selectCheck: SelectCheck = {false},
    selectChangedListener: SelectChangedListener ?= null,
    slice: PieSliceSlate = PieSlice.createDefaultSlice(values.size),
    label: LabelSlate = {},
    labelConnector: LabelConnectorSlate = LabelConnector.Straight(),
    holeSize: Float = 0f,
    holeContent: @Composable BoxScope.(contentPadding: PaddingValues) -> Unit = {},
    minPieDiameter: Dp = 100.dp,
    maxPieDiameter: Dp = 300.dp,
    forceCenteredPie: Boolean = false,
    pieAnimationSpec: AnimationSpec<Float> = KoalaPlotTheme.animationSpec,
    labelAnimationSpec: AnimationSpec<Float> = tween(LabelFadeInDuration, 0, LinearOutSlowInEasing),
    pieStartAngle: AngleUnit = AngleCCWTop.deg,
    pieExtendAngle: AngleUnit = DegreesFullCircle.deg,
) {
    Pie(
        values = values,
        labelPositionProvider = labelPositionProvider,
        modifier = modifier,
        selectCheck = selectCheck,
        selectChangedListener = selectChangedListener,
        slice = slice,
        label = label,
        labelConnector = labelConnector,
        holeSize = holeSize,
        holeContent = holeContent,
        minPieDiameter = minPieDiameter,
        maxPieDiameter = maxPieDiameter,
        forceCenteredPie = forceCenteredPie,
        pieStartAngle = pieStartAngle,
        pieExtendAngle = pieExtendAngle,
        startAnimationUseCase = StartAnimationUseCase(
            executionType = StartAnimationUseCase.ExecutionType.Default,
            pieAnimationSpec,
            labelAnimationSpec,
        )
    )
}

/**
 * Creates a Pie Chart or, optionally, a Donut Chart if holeSize is nonZero, with optional
 * hole content to place at the center of the donut hole. Pie slices are drawn starting at
 * -90 degrees (top center), progressing clockwise around the pie. Each slice occupies a fraction
 * of the overall pie according to its data value relative to the sum of all values. Places labels around the pie
 * at a minimum distance set by [labelSpacing].
 *
 * @param values The data values for each pie slice
 * @param modifier Compose Modifiers to be applied to the overall PieChart
 * @param slice Composable for a pie slice.
 * @param label Composable for a pie slice label placed around the perimeter of the pie
 * @param labelConnector Composable for label connectors connecting the pie slice to the label
 * @param labelSpacing A value greater than 1 specifying the distance from the center of
 * the pie at which to place the labels relative to the overall diameter of the pie, where a value
 * of 1 is at the outer edge of the pie. Values between 1.05 and 1.4 tend to work well depending
 * on the size of the labels and overall pie diameter.
 * @param holeSize A relative size for an inner hole of the pie, creating a donut chart, with a
 * value between 0 and 1.
 * @param holeContent Optional content that may be placed in the space of the donut hole.
 * @param minPieDiameter Minimum diameter allowed for the pie.
 * @param maxPieDiameter Maximum diameter allowed for the pie. May be Infinity but not Unspecified.
 * @param forceCenteredPie If true, will force the pie to be centered within its parent, by adjusting (decreasing) the
 * pie size to accommodate label sizes and positions. If false, will maximize the pie diameter.
 * @param pieAnimationSpec Specifies the animation to use when the pie chart is first drawn.
 * @param labelAnimationSpec Specifies the animation to use when the labels are drawn. Drawing of the labels begins
 * after the pie animation is complete.
 */
@ExperimentalKoalaPlotApi
@Composable
fun Chart.Pie(
    values: List<Float>,
    modifier: Modifier = Modifier,
    selectCheck: SelectCheck = {false},
    selectChangedListener: SelectChangedListener ?= null,
    slice: PieSliceSlate = PieSlice.createDefaultSlice(values.size),
    label: LabelSlate = {},
    labelConnector: LabelConnectorSlate = LabelConnector.Straight(),
    labelSpacing: Float = DefaultLabelDiameterScale,
    holeSize: Float = 0f,
    holeContent: @Composable BoxScope.(contentPadding: PaddingValues) -> Unit = {},
    minPieDiameter: Dp = 100.dp,
    maxPieDiameter: Dp = 300.dp,
    forceCenteredPie: Boolean = false,
    pieAnimationSpec: AnimationSpec<Float> = KoalaPlotTheme.animationSpec,
    labelAnimationSpec: AnimationSpec<Float> = tween(LabelFadeInDuration, 0, LinearOutSlowInEasing),
) {
    Pie(
        values = values,
        modifier = modifier,
        selectCheck = selectCheck,
        selectChangedListener=selectChangedListener,
        slice = slice,
        label = label,
        labelConnector = labelConnector,
        labelSpacing = labelSpacing,
        holeSize = holeSize,
        holeContent = holeContent,
        minPieDiameter = minPieDiameter,
        maxPieDiameter = maxPieDiameter,
        forceCenteredPie = forceCenteredPie,
        startAnimationUseCase =
            StartAnimationUseCase(
                executionType = StartAnimationUseCase.ExecutionType.Default,
                pieAnimationSpec,
                labelAnimationSpec,
            ),
    )
}

/**
 * Creates a Pie Chart or, optionally, a Donut Chart if holeSize is nonZero, with optional
 * hole content to place at the center of the donut hole. Pie slices are drawn starting at
 * -90 degrees (top center), progressing clockwise around the pie. Each slice occupies a fraction
 * of the overall pie according to its data value relative to the sum of all values. Places labels around the pie
 * at a minimum distance set by [labelSpacing].
 *
 * @param values The data values for each pie slice
 * @param modifier Compose Modifiers to be applied to the overall PieChart
 * @param slice Composable for a pie slice.
 * @param label Composable for a pie slice label placed around the perimeter of the pie
 * @param labelConnector Composable for label connectors connecting the pie slice to the label
 * @param labelSpacing A value greater than 1 specifying the distance from the center of
 * the pie at which to place the labels relative to the overall diameter of the pie, where a value
 * of 1 is at the outer edge of the pie. Values between 1.05 and 1.4 tend to work well depending
 * on the size of the labels and overall pie diameter.
 * @param holeSize A relative size for an inner hole of the pie, creating a donut chart, with a
 * value between 0 and 1.
 * @param holeContent Optional content that may be placed in the space of the donut hole.
 * @param minPieDiameter Minimum diameter allowed for the pie.
 * @param maxPieDiameter Maximum diameter allowed for the pie. May be Infinity but not Unspecified.
 * @param forceCenteredPie If true, will force the pie to be centered within its parent, by adjusting (decreasing) the
 * pie size to accommodate label sizes and positions. If false, will maximize the pie diameter.
 * @param startAnimationUseCase Controls the animation.
 */
@ExperimentalKoalaPlotApi
@Composable
fun Chart.Pie(
    values: List<Float>,
    modifier: Modifier = Modifier,
    selectCheck: SelectCheck = {false},
    selectChangedListener: SelectChangedListener ?= null,
    slice: PieSliceSlate = PieSlice.createDefaultSlice(values.size),
    label: @Composable (Int) -> Unit = {},
    labelConnector: LabelConnectorSlate = LabelConnector.Straight(),
    labelSpacing: Float = DefaultLabelDiameterScale,
    holeSize: Float = 0f,
    holeContent: @Composable BoxScope.(contentPadding: PaddingValues) -> Unit = {},
    minPieDiameter: Dp = 100.dp,
    maxPieDiameter: Dp = 300.dp,
    forceCenteredPie: Boolean = false,
    startAnimationUseCase: StartAnimationUseCase =
        StartAnimationUseCase(
            executionType = StartAnimationUseCase.ExecutionType.Default,
            /* chart animation */
            KoalaPlotTheme.animationSpec,
            /* label animation */
            tween(LabelFadeInDuration, 0, LinearOutSlowInEasing)
        ),
) {
    require(labelSpacing >= 1f) { "labelSpacing must be greater than 1" }
    Pie(
        values,
        CircularLabelPositionProvider(labelSpacing),
        modifier,
        selectCheck = selectCheck,
        selectChangedListener,
        slice,
        label,
        labelConnector,
        holeSize,
        holeContent,
        minPieDiameter,
        maxPieDiameter,
        forceCenteredPie,
        startAnimationUseCase = startAnimationUseCase,
    )
}

@Composable
private fun HoverableElementAreaScope.Pie(
    internalPieData: List<PieSliceData>,
    slice: PieSliceSlate,
    selectCheck: SelectCheck = {false},
    listener: SelectChangedListener ?= null,
    holeSize: Float
) {
    val sliceScopes: List<PieSliceScope> = remember(internalPieData, holeSize) {
        buildList {
            internalPieData.forEach { sliceData ->
                add(
                    PieSliceScopeImpl(
                        sliceData,
                        holeSize,
                        InitOuterRadius,
                        this@Pie
                    )
                )
            }
        }
    }

    Layout(content = {
        BoxWithConstraints {
            with(LocalDensity.current) {
                val diameter = min(constraints.maxWidth, constraints.maxHeight)
                val sizeModifier = Modifier.size(diameter.toDp())
                Box(modifier = sizeModifier) {
                    internalPieData.forEachIndexed { index, _ ->
                        with(sliceScopes[index]) {
                            slice(index,selectCheck(index),listener)
                        }
                    }
                }
            }
        }
    }) { measurables, constraints ->
        val chart = measurables[0].measure(constraints)

        layout(constraints.maxWidth, constraints.maxHeight) {
            chart.place(0, 0)
        }
    }
}


/**
 * Creates a Pie Chart or, optionally, a Donut Chart if holeSize is nonZero, with optional
 * hole content to place at the center of the donut hole. Pie slices are drawn starting at
 * -90 degrees (top center), progressing clockwise around the pie. Each slice occupies a fraction
 * of the overall pie according to its data value relative to the sum of all values.
 *
 * @param values The data values for each pie slice
 * @param labelPositionProvider A provider of label offsets that can be used to implement different label
 * placement strategies. See the [PieChart] override for a version that uses labels placed around the circumference
 * of the pie.
 * @param modifier Compose Modifiers to be applied to the overall PieChart
 * @param slice Composable for a pie slice.
 * @param label Composable for a pie slice label placed around the perimeter of the pie
 * @param labelConnector Composable for label connectors connecting the pie slice to the label
 * @param holeSize A relative size for an inner hole of the pie, creating a donut chart, with a
 * value between 0 and 1.
 * @param holeContent Optional content that may be placed in the space of the donut hole. To safely draw the content
 * within the hole without intersecting with the chart, apply the passed `contentPadding` to your content composable.
 * @param minPieDiameter Minimum diameter allowed for the pie.
 * @param maxPieDiameter Maximum diameter allowed for the pie. May be Infinity but not Unspecified.
 * @param forceCenteredPie If true, will force the pie to be centered within its parent, by adjusting (decreasing) the
 * pie size to accommodate label sizes and positions. If false, will maximize the pie diameter.
 * @param startAnimationUseCase Controls the animation.
 * @param pieStartAngle Sets an angle for the pie data to start at. Defaults to the top of the pie.
 * @param pieExtendAngle Sets a max angle for the pie to extend to, with a value between 1 and 360.
 * Defaults to [DegreesFullCircle].
 */
@ExperimentalKoalaPlotApi
@Composable
fun Chart.Pie(
    values: List<Float>,
    labelPositionProvider: LabelPositionProvider,
    modifier: Modifier = Modifier,
    selectCheck: SelectCheck = {false},
    selectChangedListener: SelectChangedListener ?= null,
    slice: PieSliceSlate = PieSlice.createDefaultSlice(values.size),
    label: LabelSlate = {},
    labelConnector: LabelConnectorSlate = LabelConnector.Straight(),
    holeSize: Float = 0f,
    holeContent: @Composable BoxScope.(contentPadding: PaddingValues) -> Unit = {},
    minPieDiameter: Dp = 100.dp,
    maxPieDiameter: Dp = 300.dp,
    forceCenteredPie: Boolean = false,
    startAnimationUseCase: StartAnimationUseCase =
        StartAnimationUseCase(
            executionType = StartAnimationUseCase.ExecutionType.Default,
            /* chart animation */
            KoalaPlotTheme.animationSpec,
            /* label animation */
            tween(LabelFadeInDuration, 0, LinearOutSlowInEasing)
        ),
    pieStartAngle: AngleUnit = AngleCCWTop.toDegrees(),
    pieExtendAngle: AngleUnit = DegreesFullCircle.toDegrees(),
) {
    require(holeSize in 0f..1f) { "holeSize must be between 0 and 1" }
    require(maxPieDiameter != Dp.Unspecified) { "maxPieDiameter cannot be Unspecified" }
    require(pieExtendAngle.toDegrees().value > 0f && pieExtendAngle.toDegrees().value <= DegreesFullCircle) {
        "pieExtendAngle must be between 0 and 360, exclusive of 0"
    }
    require(startAnimationUseCase.animatables.size == 2) { "startAnimationUseCase must have 2 animatables" }

    val currentValues by rememberUpdatedState(values)
    val beta = remember(values) { startAnimationUseCase.animatables[0] }
    val labelAlpha = remember(values) { startAnimationUseCase.animatables[1] }

    startAnimationUseCase(key = values)

    // pieSliceData that gets animated - used for drawing the pie
    val pieSliceData by remember(beta.value) {
        derivedStateOf {
            makePieSliceData(
                currentValues,
                beta.value,
                pieStartAngle,
                pieExtendAngle
            )
        }
    }

    // pieSliceData when the animation is complete - used for sizing & label layout/positioning
    val finalPieSliceData by remember {
        derivedStateOf {
            makePieSliceData(
                currentValues,
                1f,
                pieStartAngle,
                pieExtendAngle
            )
        }
    }

    HoverableElementArea(modifier = modifier) {
        SubcomposeLayout(modifier = Modifier.clipToBounds()) { constraints ->
            val pieMeasurePolicy =
                PieMeasurePolicy(finalPieSliceData, holeSize, labelPositionProvider,
                    InitOuterRadius, forceCenteredPie)

            val pieMeasurable = subcompose("pie") { Pie(pieSliceData, slice, selectCheck,selectChangedListener , holeSize) }[0]

            val labelMeasurables = subcompose("labels") {
                pieSliceData.indices.forEach { index ->
                    // Wrapping in box ensures there is 1 measurable element
                    // emitted per label & applies fade animation
                    Box(modifier = Modifier.alpha(labelAlpha.value)) { label(index) }
                }
            }

            val (pieDiameter, piePlaceable, labelPlaceables) = pieMeasurePolicy.measure(
                pieMeasurable,
                labelMeasurables,
                constraints,
                minPieDiameter.toPx(),
                maxPieDiameter.toPx()
            )

            val labelPositions =
                labelPositionProvider.computeLabelPositions(
                    pieDiameter * InitOuterRadius,
                    holeSize,
                    labelPlaceables,
                    finalPieSliceData
                )

            val size = pieMeasurePolicy.computeSize(labelPlaceables, labelPositions, pieDiameter).run {
                // add one due to later float to int conversion dropping the fraction part
                copy(
                    (width + 1).coerceAtMost(constraints.maxWidth.toFloat()),
                    (height + 1).coerceAtMost(constraints.maxHeight.toFloat())
                )
            }

            val labelConnectorTranslations = pieMeasurePolicy.computeLabelConnectorScopes(labelPositions, pieDiameter)

            val holeDiameter = pieDiameter * holeSize.toDouble()
            val holeSafeEdgeLength = circumscribedSquareSize(holeDiameter)
            val holePlaceable = subcompose("hole") {
                Box(modifier = Modifier.clip(CircleShape)) {
                    holeContent(PaddingValues((holeDiameter - holeSafeEdgeLength).toInt().dp))
                }
            }[0].measure(Constraints.fixed(holeDiameter.toInt(), holeDiameter.toInt()))

            val connectorPlaceables = pieSliceData.mapIndexed { index, _ ->
                subcompose("connector $index") {
                    Box(modifier = Modifier.fillMaxSize().alpha(labelAlpha.value)) {
                        labelConnectorTranslations[index]?.let {
                            with(it.second) { labelConnector(index) }
                        }
                    }
                }.map { it.measure(constraints) }
            }.flatten()

            with(pieMeasurePolicy) {
                layoutPie(
                    size,
                    labelPositions,
                    labelConnectorTranslations.map { it?.first },
                    pieDiameter,
                    PiePlaceables(piePlaceable, holePlaceable, labelPlaceables, connectorPlaceables)
                )
            }
        }
    }
}
